SizeEstimation.estimateUtxo   D
last analyzed

Complexity

Conditions 9
Paths 6

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
c 1
b 0
f 0
nc 6
dl 0
loc 40
rs 4.909
nop 2
1
var assert = require('assert');
2
var bitcoin = require('bitcoinjs-lib');
3
4
var SizeEstimation = {
5
    SIZE_DER_SIGNATURE: 72,
6
    SIZE_V0_P2WSH: 36
7
};
8
9
SizeEstimation.getPublicKeySize = function(isCompressed) {
10
    return isCompressed ? 33 : 65;
11
};
12
13
SizeEstimation.getLengthForScriptPush = function(length) {
14
    if (length < 75) {
15
        return 1;
16
    } else if (length <= 0xff) {
17
        return 2;
18
    } else if (length <= 0xffff) {
19
        return 3;
20
    } else if (length <= 0xffffffff) {
21
        return 5;
22
    } else {
23
        throw new Error("Size of pushdata too large");
24
    }
25
};
26
27
SizeEstimation.getLengthForVarInt = function(length) {
28
    if (length < 253) {
29
        return 1;
30
    }
31
32
    // Rest have a prefix byte
33
    var numBytes;
34
    if (length < 65535) {
35
        numBytes = 2;
36
    } else if (length < 4294967295) {
37
        numBytes = 4;
38
    } else if (length < 18446744073709551615) {
39
        numBytes = 8;
40
    } else {
41
        throw new Error("Size of varint too large");
42
    }
43
44
    return 1 + numBytes;
45
};
46
47
SizeEstimation.estimateMultisigStackSize = function(m, keys) {
48
    // Initialize with OP_0
49
    var stackSizes = [0];
50
    var i;
51
    for (i = 0; i < m; i++) {
52
        stackSizes.push(SizeEstimation.SIZE_DER_SIGNATURE);
53
    }
54
55
    var scriptSize = 1; // OP_$m
56
    for (i = 0; i < keys.length; i++) {
57
        scriptSize += this.getLengthForScriptPush(keys[i].length) + keys[i].length;
58
    }
59
    scriptSize += 2; // OP_$n OP_CHECKMULTISIG
60
    return [stackSizes, scriptSize];
61
};
62
63
SizeEstimation.estimateP2PKStackSize = function(key) {
64
    var stackSizes = [SizeEstimation.SIZE_DER_SIGNATURE];
65
    var scriptSize = this.getLengthForScriptPush(key.length) + key.length + 1; // KEY OP_CHECKSIG
66
67
    return [stackSizes, scriptSize];
68
};
69
70
SizeEstimation.estimateP2PKHStackSize = function(isCompressed) {
71
    if (typeof isCompressed === 'undefined') {
72
        isCompressed = true;
73
    }
74
75
    var stackSizes = [this.SIZE_DER_SIGNATURE, this.getPublicKeySize(isCompressed)];
76
    var scriptSize = 2 + this.getLengthForScriptPush(20) + 20 + 2;
77
78
    return [stackSizes, scriptSize];
79
};
80
81
/**
82
 * As pure a function as it gets, but without the overhead
83
 * of checking everything. Make sure your calls are correct.
84
 *
85
 * @param {Buffer} stackSizes
86
 * @param {Buffer} isWitness
87
 * @param {Buffer} rs
88
 * @param {Buffer} ws
89
 */
90
SizeEstimation.estimateStackSignatureSize = function(stackSizes, isWitness, rs, ws) {
91
    assert(ws === null || isWitness);
92
93
    var scriptSigSizes = [];
94
    var witnessSizes = [];
95
    if (isWitness) {
96
        witnessSizes = stackSizes;
97
        if (ws instanceof Buffer) {
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
98
            witnessSizes.push(ws.length);
99
        }
100
    } else {
101
        scriptSigSizes = stackSizes;
102
    }
103
104
    if (rs instanceof Buffer) {
105
        scriptSigSizes.push(rs.length);
106
    }
107
108
    var self = this;
109
    var scriptSigSize = 0;
110
    scriptSigSizes.map(function(elementLen) {
111
        scriptSigSize += self.getLengthForScriptPush(elementLen) + elementLen;
112
    });
113
114
    scriptSigSize += self.getLengthForVarInt(scriptSigSize);
115
116
    var witnessSize = 0;
117
    if (witnessSizes.length > 0) {
118
        witnessSizes.map(function(elementLen) {
119
            witnessSize += self.getLengthForVarInt(elementLen) + elementLen;
120
        });
121
        witnessSize += self.getLengthForVarInt(witnessSizes.length);
122
    }
123
124
    return [scriptSigSize, witnessSize];
125
};
126
127
/**
128
 *
129
 * @param {Buffer} script - main script, can equal RS/WS too
130
 * @param {Buffer} redeemScript
131
 * @param {Buffer} witnessScript
132
 * @param {boolean} isWitness - required, covers P2WPKH and so on
133
 * @param {boolean} compressed - only strictly required for p2pkh
134
 */
135
SizeEstimation.estimateInputFromScripts = function(script, redeemScript, witnessScript, isWitness, compressed) {
136
    assert(witnessScript === null || isWitness);
137
138
    var stackSizes;
139
    if (bitcoin.script.multisig.output.check(script)) {
140
        var multisig = bitcoin.script.multisig.output.decode(script);
141
        stackSizes = this.estimateMultisigStackSize(multisig.m, multisig.pubKeys)[0];
142
    } else if (bitcoin.script.pubKey.output.check(script)) {
143
        var p2pk = bitcoin.script.pubKey.output.decode(script);
144
        stackSizes = this.estimateP2PKStackSize(p2pk)[0];
145
    } else if (bitcoin.script.pubKeyHash.output.check(script)) {
146
        stackSizes = this.estimateP2PKHStackSize(compressed)[0];
147
    } else {
148
        throw new Error("Unsupported script type");
149
    }
150
151
    return this.estimateStackSignatureSize(stackSizes, isWitness, redeemScript, witnessScript);
152
};
153
154
SizeEstimation.estimateUtxo = function(utxo, compressed) {
155
    var spk = Buffer.from(utxo.scriptpubkey_hex, 'hex');
0 ignored issues
show
Bug introduced by
The variable Buffer seems to be never declared. If this is a global, consider adding a /** global: Buffer */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
156
    var rs = typeof utxo.redeem_script === 'string' ? Buffer.from(utxo.redeem_script, 'hex') : null;
157
    var ws = typeof utxo.witness_script === 'string' ? Buffer.from(utxo.witness_script, 'hex') : null;
158
    var witness = false;
159
160
    var signScript = spk;
161
    if (bitcoin.script.scriptHash.output.check(signScript)) {
162
        if (null === rs) {
163
            throw new Error("Cant estimate, missing redeem script");
164
        }
165
        signScript = rs;
166
    }
167
168
    if (bitcoin.script.witnessPubKeyHash.output.check(signScript)) {
169
        var p2wpkh = bitcoin.script.witnessPubKeyHash.output.decode(signScript);
170
        signScript = bitcoin.script.pubKeyHash.output.encode(p2wpkh);
171
        witness = true;
172
    } else if (bitcoin.script.witnessScriptHash.output.check(signScript)) {
173
        if (null === ws) {
174
            throw new Error("Can't estimate, missing witness script");
175
        }
176
        signScript = ws;
177
        witness = true;
178
    }
179
180
    var types = bitcoin.script.types;
181
    var allowedTypes = [types.MULTISIG, types.P2PKH, types.P2PK];
182
    var type = bitcoin.script.classifyOutput(signScript);
183
    if (allowedTypes.indexOf(type) === -1) {
184
        throw new Error("Unsupported script type");
185
    }
186
187
    var estimation = this.estimateInputFromScripts(signScript, rs, ws, witness, compressed);
188
189
    return {
190
        scriptSig: estimation[0],
191
        witness: estimation[1]
192
    };
193
};
194
195
/**
196
 * Returns the size of input related data, given a set
197
 * of utxos we can estimate for. witness data is included
198
 * if withWitness=true, and is required for vsize/weight
199
 * calculations
200
 * @param {object} utxos
201
 * @param {boolean} withWitness
202
 * @returns {number}
203
 */
204
SizeEstimation.estimateInputsSize = function(utxos, withWitness) {
205
    var inputSize = 0;
206
    var witnessSize = 0;
207
    utxos.map(function(utxo) {
208
        var estimate = SizeEstimation.estimateUtxo(utxo);
209
        // txid + vout + sequence + scriptSig
210
        inputSize += 32 + 4 + 4 + estimate.scriptSig;
211
        if (withWitness) {
212
            witnessSize += estimate.witness;
213
        }
214
    });
215
216
    if (withWitness && witnessSize > 0) {
217
        inputSize += 2 + witnessSize;
218
    }
219
220
    return inputSize;
221
};
222
223
/**
224
 * Calculates number of bytes to serialize tx outputs
225
 * @param {Array} outs
226
 * @returns {number}
227
 */
228
SizeEstimation.calculateOutputsSize = function(outs) {
229
    var outputsSize = 0;
230
    outs.map(function(out) {
231
        var scriptSize = SizeEstimation.getLengthForVarInt(out.script.length);
232
        outputsSize += 8 + scriptSize + (out.script.length);
233
    });
234
    return outputsSize;
235
};
236
237
/**
238
 * Returns the transactions weight.
239
 *
240
 * @param {bitcoin.Transaction} tx
241
 * @param {array} utxos
242
 * @returns {*}
243
 */
244
SizeEstimation.estimateTxWeight = function(tx, utxos) {
245
    var outSize = SizeEstimation.calculateOutputsSize(tx.outs);
246
    // version + vinLen + vin + voutLen + vout + nlocktime
247
    var baseSize = 4 + SizeEstimation.getLengthForVarInt(utxos.length) + this.estimateInputsSize(utxos, false) +
248
        SizeEstimation.getLengthForVarInt(tx.outs.length) + outSize + 4;
249
    // version + vinLen + vin (includes witness) + voutLen + vout + nlocktime
250
    var witSize = 4 + SizeEstimation.getLengthForVarInt(utxos.length) + this.estimateInputsSize(utxos, true) +
251
        SizeEstimation.getLengthForVarInt(tx.outs.length) + outSize + 4;
252
253
    return (3 * baseSize) + witSize;
254
};
255
256
/**
257
 * Returns the vsize for a transaction. Same as size
258
 * for transaction with no witness data.
259
 *
260
 * @param {bitcoin.Transaction} tx
261
 * @param {array} utxos
262
 * @returns {Number}
263
 */
264
SizeEstimation.estimateTxVsize = function(tx, utxos) {
265
    return parseInt(Math.ceil(SizeEstimation.estimateTxWeight(tx, utxos) / 4), 10);
266
};
267
268
module.exports = SizeEstimation;
269